namespace egret.socketio {

    /**
        * path (String) name of the path that is captured on the server side (/socket.io)
        * reconnection (Boolean) whether to reconnect automatically (true)
        * reconnectionAttempts (Number) number of reconnection attempts before giving up (Infinity)
        * reconnectionDelay (Number) how long to initially wait before attempting a new reconnection (1000). Affected by +/- randomizationFactor, for example the default initial delay will be between 500 to 1500ms.
        * reconnectionDelayMax (Number) maximum amount of time to wait between
        * reconnections (5000). Each attempt increases the reconnection delay by 2x along with a randomization as above 
        * randomizationFactor (Number) (0.5), 0 <= randomizationFactor <= 1
        * timeout (Number) connection timeout before a connect_error and connect_timeout events are emitted (20000)
        * autoConnect (Boolean) by setting this false, you have to call manager.open whenever you decide it's appropriate
        * query (Object): additional query parameters that are sent when connecting a namespace (then found in socket.handshake.query object on the server-side)
        * parser (Parser): the parser to use. Defaults to an instance of the Parser that ships with socket.io. See socket.io-parser.
        */
    export interface ManagerOptions {
        path?: string;
        reconnection?: boolean;
        reconnectionAttempts?: number;
        reconnectionDelay?: number;
        reconnectionDelayMax?: number;
        randomizationFactor?: number;
        timeout?: number;
        autoConnect?: boolean;
        parser?: Parser;
        transports?: Array<any>;
    }


    /**
         * Manager
         */
    export class Manager extends Emitter {

        readyState: _emReadyState;

        private _reconnection: boolean;
        private skipReconnect: boolean;
        private reconnecting: boolean;
        private _reconnectionAttempts: number;
        private _reconnectionDelay: number;
        private _reconnectionDelayMax: number;
        private _randomizationFactor: number;
        private _timeout: number;
        private backoff: Backoff;


        private uri: Uri;
        private opts: ManagerOptions;

        public nsps: { [name: string]: Socket } = {};
        private subs: Handle[] = [];

        private encoding: boolean;
        private decoding: boolean;
        private encoder: parser.Encoder;
        private decoder: parser.Decoder;

        private connecting = [];
        private packetBuffer = [];

        public autoConnect: boolean;
        private lastPing: Date;

        private engine: engine.Socket;

        constructor(uri?: Uri, opts?: ManagerOptions) {
            super();

            opts = opts || {
                timeout: 20000,
                reconnection: true,
            };


            uri.path = opts.path || '/socket.io';
            // opts.uri = opts.uri || new engine.SocketUri(uri.toString());
            // opts.uri.path = opts.path || '/socket.io';
            opts.reconnection = opts.reconnection || true;

            this.opts = opts;

            this.reconnection(opts.reconnection);
            this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
            this.reconnectionDelay(opts.reconnectionDelay || 1000);
            this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
            this.randomizationFactor(opts.randomizationFactor || 0.5);
            this.timeout(opts.timeout || 20000);
            this.backoff = new Backoff({
                min: this.reconnectionDelay(),
                max: this.reconnectionDelayMax(),
                jitter: this.randomizationFactor()
            });

            this.readyState = _emReadyState.CLOSED;
            this.connecting = [];
            this.lastPing = null;
            this.encoding = false;
            this.uri = uri;
            this.encoder = opts.parser && opts.parser.Encoder || new parser.Encoder();
            this.decoder = opts.parser && opts.parser.Decoder || new parser.Decoder();
            this.autoConnect = opts.autoConnect !== false;
            // if (this.autoConnect) this.open();
        }

        private emitAll(event: string, ...args): void {
            this.emit(event, args);

            for (let key in this.nsps) {
                this.nsps[key].emit(event, args);
            }
        }

        private updateSocketIds(): void {
            for (let key in this.nsps) {
                this.nsps[key].id = this.generateId(key);
            }
        }

        private generateId(nsp: string): string {
            return (("/" === nsp) ? "" : (nsp + "#") + this.engine.id);
        }

        public reconnection(v?: boolean) {
            if (!v)
                return this._reconnection;

            this._reconnection = v;
            return this;
        }

        public reconnectionAttempts(v?: number) {
            if (!v)
                return this._reconnectionAttempts;
            this._reconnectionAttempts = v;
            return this;
        }

        public reconnectionDelay(v?: number) {
            if (!v)
                return this._reconnectionDelay;
            this._reconnectionDelay = v;
            this.backoff && this.backoff.setMin(v);
            return this;
        }

        public randomizationFactor(v?: number) {
            if (!v)
                return this._randomizationFactor;
            this._randomizationFactor = v;
            this.backoff && this.backoff.setJitter(v);
            return this;
        }

        public reconnectionDelayMax(v?: number) {
            if (!v)
                return this._reconnectionDelayMax;
            this._reconnectionDelayMax;
            this.backoff && this.backoff.setMax(v);
            return this;
        }

        public timeout(v?: number) {
            if (!v)
                return this._timeout;
            this._timeout = v;
            return this;
        }

        public maybeReconnectOnOpen(): void {
            if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) {
                this.reconnect();
            }
        }

        public open(fn?: Function, opts?) {

            if (this.readyState === _emReadyState.OPEN) return this;

            let uri = new engine.SocketUri(this.uri.toString());
            uri.transports = this.opts.transports;

            this.engine = new engine.Socket(uri);

            let socket = this.engine;
            this.readyState = _emReadyState.OPENING;
            this.skipReconnect = false;

            let openSub = on(socket, _emEvent.EVENT_OPEN, () => {
                this.onopen();
                fn && fn();
            });

            let errorSub = on(socket, _emEvent.EVENT_ERROR, (data) => {
                this.cleanup();
                this.readyState = _emReadyState.CLOSED;
                this.emitAll(_emEvent.EVENT_CONNECT_ERROR, data);
                if (fn) {
                    let err = new Error('Connection error');
                    err.data = data;
                    fn(err);
                } else {
                    this.maybeReconnectOnOpen();
                }
            });

            if (this._timeout) {
                let timeout = this._timeout;

                let timer = setTimeout(() => {
                    openSub.destory();
                    socket.close();
                    socket.emit(_emEvent.EVENT_ERROR, 'timeout');
                    this.emitAll(_emEvent.EVENT_CONNECT_ERROR, timeout);
                }, this, timeout);

                this.subs.push({
                    destory: (): void => {
                        clearTimeout(timer);
                    }
                });
            }

            this.subs.push(openSub);
            this.subs.push(errorSub);

            return this;
        }

        private onopen() {
            console.debug('open');

            this.cleanup();

            this.readyState = _emReadyState.OPEN;
            this.emit(_emEvent.EVENT_OPEN);

            let socket = this.engine;

            this.subs.push(on(socket, _emEvent.EVENT_DATA, this.ondata, this));
            this.subs.push(on(socket, _emEvent.EVENT_PING, this.onping, this));
            this.subs.push(on(socket, _emEvent.EVENT_PONG, this.onpong, this));
            this.subs.push(on(socket, _emEvent.EVENT_ERROR, this.onerror, this));
            this.subs.push(on(socket, _emEvent.EVENT_CLOSE, this.onclose, this));
            this.subs.push(on(this.decoder, _emEvent.EVENT_DECODED, this.ondecoded, this));
        }

        private cleanup(): void {
            let subsLength = this.subs.length;
            for (let i = 0; i < subsLength; i++) {
                let sub = this.subs.shift();
                sub.destory && sub.destory();
            }

            this.encoding = false;
            this.lastPing = null;

            this.decoder.destory();
        }

        public socket(nsp: string, opts?: ManagerOptions): Socket {
            let socket = this.nsps[nsp];

            if (!socket) {
                let onConnecting = () => {
                    if (!this.connecting.indexOf(socket)) {
                        this.connecting.push(socket);
                    }
                }

                socket = new Socket(this, nsp, this.uri);
                this.nsps[nsp] = socket;

                socket.on(_emEvent.EVENT_CONNECTING, onConnecting);
                socket.on(_emEvent.EVENT_CONNECT, () => {
                    socket.id = this.generateId(nsp);
                });

                if (this.autoConnect) {
                    onConnecting();
                }
            }

            return socket;
        }

        public packet(packet: Packet): void {
            console.debug('writing packet', packet);
            if (packet.query && packet.type === 0) packet.nsp += '?' + packet.query;

            if (!this.encoding) {
                // encode, then write to engine with result
                this.encoding = true;
                this.encoder.encode(packet, (encodedPackets) => {
                    for (var i = 0; i < encodedPackets.length; i++) {
                        this.engine.write(encodedPackets[i], packet.options);
                    }
                    this.encoding = false;
                    this.processPacketQueue();
                });
            } else { // add packet to the queue
                this.packetBuffer.push(packet);
            }
        }

        public reconnect(): Manager {
            if (this.reconnecting || this.skipReconnect)
                return this;

            if (this.backoff.attempts >= this._reconnectionAttempts) {
                console.debug('reconnect failed');
                this.backoff.reset();
                this.emitAll('reconnect_failed');
                this.reconnecting = false;
            } else {
                let delay = this.backoff.duration();
                console.debug('will wait ' + delay + 'dms before reconnect attempt');
                this.reconnecting = true;

                let timer = setTimeout(() => {
                    if (this.skipReconnect) return;

                    this.emitAll(_emEvent.EVENT_RECONNECT_ATTEMPT, this.backoff.attempts);
                    this.emitAll(_emEvent.EVENT_RECONNECTING, this.backoff.attempts);

                    if (this.skipReconnect) return;

                    this.open((err) => {
                        if (err) {
                            console.debug('reconnect attempt error');
                            this.reconnecting = false;
                            this.reconnect();
                            this.emitAll(_emEvent.EVENT_RECONNECT_ERROR, err.data);
                        } else {
                            console.debug('reconnect success');
                            this.onreconnect();
                        }
                    });
                }, this, delay);

                this.subs.push({
                    destory: () => {
                        clearTimeout(timer);
                    }
                });
            }

        }

        public destroy(socket: Socket): void {
            let index = this.connecting.indexOf(socket);
            if (index > 0) this.connecting.splice(index, 1);
            if (this.connecting.length) return;

            this.close();
        }

        private processPacketQueue(): void {
            if (this.packetBuffer.length > 0 && !this.encoding) {
                let pack = this.packetBuffer.shift();
                this.packet(pack);
            }
        }

        private close(): void {
            return this.disconnect();
        }

        private disconnect(): void {
            console.debug('disconnect');
            this.skipReconnect = true;
            this.reconnecting = false;
            if (this.readyState === _emReadyState.OPENING) {
                this.cleanup();
            }
            this.backoff.reset();
            this.readyState = _emReadyState.CLOSED;
            if (this.engine)
                this.engine.close();
        }

        private ondata(data: any): void {
            this.decoder.add(data);
        }

        private onping(): void {
            this.lastPing = new Date();
            this.emitAll(_emEvent.EVENT_PING);
        }

        private onpong(): void {
            this.emitAll(_emEvent.EVENT_PONG, (new Date().getDate() - this.lastPing.getDate()));
        }

        private onerror(err: Error): void {
            console.error(_emEvent.EVENT_ERROR, err);
            this.emitAll(_emEvent.EVENT_ERROR, err);
        }

        private onclose(reasion?: string): void {
            console.debug('onclose');

            this.cleanup();
            this.backoff.reset();
            this.readyState = _emReadyState.CLOSED;
            this.emit(_emEvent.EVENT_CLOSE, reasion);

            if (this._reconnection && !this.skipReconnect) {
                this.reconnect();
            }
        }

        private ondecoded(packet: Packet): void {
            this.emit(_emEvent.EVENT_PACKET, packet);
        }

        private onreconnect(): void {
            let attempt = this.backoff.attempts;
            this.reconnecting = false;
            this.backoff.reset();
            this.updateSocketIds();
            this.emitAll(_emEvent.EVENT_RECONNECT, attempt);
        }
    }
}